<template>
  <div class="max-w-4xl mx-auto p-6">
    <h1 class="text-2xl font-bold mb-4">视频压缩（浏览器端）</h1>

    <div class="bg-white shadow rounded p-4 mb-6">
      <p class="text-sm text-gray-600 mb-4">将视频文件上传后，页面会把视频绘制到 canvas 并使用 MediaRecorder 以更低分辨率 / 码率重新编码，最后生成可下载文件。此方法无需上传服务器，适合常见小文件处理。浏览器兼容性参见说明。</p>

      <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
        <div>
          <label class="block text-sm font-medium text-gray-700 mb-2">选择视频文件</label>
          <input type="file" accept="video/*" @change="onFileChange" class="mb-3" />

          <div v-if="originalUrl">
            <h4 class="text-sm font-medium text-gray-700 mb-2">原始预览</h4>
            <video ref="originalVideo" :src="originalUrl" controls class="w-full bg-black rounded"></video>
            <p class="text-xs text-gray-500 mt-2">时长：{{formatTime(duration)}} · 大小：{{formatBytes(originalSize)}}</p>
          </div>
          <div v-else class="w-full h-48 bg-gray-50 rounded flex items-center justify-center text-gray-400">还没有选择视频</div>
        </div>

        <div>
          <h4 class="text-sm font-medium text-gray-700 mb-2">压缩设置</h4>

          <div class="mb-3">
            <label class="block text-sm text-gray-600 mb-1">分辨率</label>
            <select v-model="preset" class="w-full border rounded px-2 py-1">
              <option value="same">与原始相同</option>
              <option value="720">720p（1280×x）</option>
              <option value="480">480p（854×x）</option>
              <option value="360">360p（640×x）</option>
            </select>
          </div>

          <div class="mb-3">
            <label class="block text-sm text-gray-600 mb-1">目标码率（kb/s，可选）</label>
            <input type="number" v-model.number="customKbps" placeholder="留空使用预设" class="w-full border rounded px-2 py-1" />
            <p class="text-xs text-gray-500 mt-1">如果留空，页面会根据分辨率选择一个合理的默认值。</p>
          </div>

          <div class="flex space-x-3 mt-4">
            <button @click="startCompress" :disabled="!readyToStart || isProcessing" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50">开始压缩</button>
            <button @click="stopCompress" :disabled="!isProcessing" class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 disabled:opacity-50">停止</button>
            <button @click="downloadCompressed" :disabled="!compressedBlob" class="px-4 py-2 bg-gray-700 text-white rounded hover:bg-gray-800 disabled:opacity-50">下载结果</button>
          </div>

          <div v-if="isProcessing" class="mt-4">
            <p class="text-sm text-gray-600">进度：{{ Math.floor(progress * 100) }}%</p>
            <div class="w-full bg-gray-200 rounded h-2 mt-2 overflow-hidden"><div :style="{width: (progress*100)+'%'}" class="h-2 bg-blue-600"></div></div>
          </div>

          <div v-if="compressedBlob" class="mt-4">
            <p class="text-sm text-gray-600">压缩完成：大小 {{ formatBytes(compressedSize) }}，节省 {{ ((1 - compressedSize/originalSize) * 100).toFixed(1) }}%</p>
            <video :src="compressedUrl" controls class="w-full bg-black rounded mt-2"></video>
          </div>
        </div>
      </div>

      <div class="text-xs text-gray-500">
        <p>注意：浏览器端压缩效果依赖 MediaRecorder 支持的编码器，生成格式通常为 WebM（VP8/VP9 + Opus）。若浏览器不支持 MediaRecorder 或 captureStream，此功能可能不可用。</p>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onUnmounted, nextTick } from 'vue'

const originalFile = ref(null)
const originalUrl = ref(null)
const originalVideo = ref(null)
const duration = ref(0)
const originalSize = ref(0)

const preset = ref('480')
const customKbps = ref(null)

const isProcessing = ref(false)
const progress = ref(0)

let canvas = null
let canvasCtx = null
let drawRaf = null
let captureStream = null
let mediaRecorder = null
let recordedChunks = []
let audioCtx = null
let audioSource = null
let audioDest = null

const compressedBlob = ref(null)
const compressedUrl = ref(null)
const compressedSize = ref(0)

function formatBytes(bytes) {
  if (!bytes) return '0 B'
  const units = ['B','KB','MB','GB']
  let i = 0
  let v = bytes
  while (v >= 1024 && i < units.length-1) { v/=1024; i++ }
  return v.toFixed(2) + ' ' + units[i]
}

function formatTime(s) {
  if (!s) return '0:00'
  const m = Math.floor(s/60)
  const sec = Math.floor(s%60).toString().padStart(2,'0')
  return `${m}:${sec}`
}

function onFileChange(e) {
  stopCompress()
  const file = e.target.files && e.target.files[0]
  if (!file) return
  originalFile.value = file
  originalSize.value = file.size
  if (originalUrl.value) URL.revokeObjectURL(originalUrl.value)
  originalUrl.value = URL.createObjectURL(file)
  compressedBlob.value = null
  compressedUrl.value = null

  // load metadata to get duration
  nextTick(() => {
    const vid = originalVideo.value
    if (!vid) return
    vid.onloadedmetadata = () => {
      duration.value = vid.duration || 0
    }
    vid.load()
  })
}

function choosePresetValues() {
  // return { width, kbps }
  if (preset.value === 'same') {
    const vid = originalVideo.value
    const w = (vid && vid.videoWidth) ? vid.videoWidth : 1280
    // choose kbps by width
    const kbps = Math.round((w/1280) * 2500)
    return { width: w, kbps }
  }
  if (preset.value === '720') return { width: 1280, kbps: 2500 }
  if (preset.value === '480') return { width: 854, kbps: 1200 }
  if (preset.value === '360') return { width: 640, kbps: 700 }
  return { width: 854, kbps: 1200 }
}

const readyToStart = computed(() => !!originalFile.value && !!originalUrl.value && !isProcessing.value)

async function startCompress() {
  if (!originalFile.value) { alert('请先选择视频文件'); return }
  if (!('MediaRecorder' in window) || !HTMLCanvasElement.prototype.captureStream) {
    alert('当前浏览器不支持必要的 API（MediaRecorder 或 canvas.captureStream）')
    return
  }

  const vid = originalVideo.value
  if (!vid) return

  // ensure metadata loaded
  if (!vid.duration || !vid.videoWidth) {
    await new Promise((res) => { vid.onloadedmetadata = res; vid.load() })
  }

  const presetVals = choosePresetValues()
  const targetWidth = presetVals.width
  const kbps = customKbps.value && customKbps.value > 0 ? customKbps.value : presetVals.kbps

  const aspect = vid.videoHeight ? (vid.videoHeight / vid.videoWidth) : (9/16)
  const targetHeight = Math.round(targetWidth * aspect)

  // create canvas
  canvas = document.createElement('canvas')
  canvas.width = targetWidth
  canvas.height = targetHeight
  canvasCtx = canvas.getContext('2d')

  // capture stream from canvas
  const fps = 25
  captureStream = canvas.captureStream(fps)

  // 尝试把原视频的音频通过 WebAudio 加入到捕获流中（以保留声音）
  try {
    audioCtx = new (window.AudioContext || window.webkitAudioContext)()
    audioSource = audioCtx.createMediaElementSource(vid)
    audioDest = audioCtx.createMediaStreamDestination()
    audioSource.connect(audioDest)
    // 将音轨添加到 canvas 捕获流
    audioDest.stream.getAudioTracks().forEach(t => captureStream.addTrack(t))
  } catch (e) {
    // 有些情况下（跨域或浏览器限制）无法创建 MediaElementSource，静默降级
    console.warn('无法将音频加入捕获流，压缩结果可能没有声音：', e)
  }

  // choose mimeType
  let mimeType = ''
  if (MediaRecorder.isTypeSupported('video/webm;codecs=vp9')) mimeType = 'video/webm;codecs=vp9'
  else if (MediaRecorder.isTypeSupported('video/webm;codecs=vp8')) mimeType = 'video/webm;codecs=vp8'
  else mimeType = 'video/webm'

  const bitsPerSecond = kbps * 1000

  try {
    recordedChunks = []
    mediaRecorder = new MediaRecorder(captureStream, { mimeType, bitsPerSecond })
  } catch (err) {
    console.error('无法创建 MediaRecorder：', err)
    alert('无法创建 MediaRecorder（可能是不支持的编码或参数）')
    return
  }

  mediaRecorder.ondataavailable = e => { if (e.data && e.data.size > 0) recordedChunks.push(e.data) }
  mediaRecorder.onstop = onRecorderStop

  // draw loop
  function drawLoop() {
    if (!vid || vid.paused || vid.ended) {
      // still draw last frame if needed
      if (vid && !vid.ended) canvasCtx.drawImage(vid, 0, 0, canvas.width, canvas.height)
      drawRaf = requestAnimationFrame(drawLoop)
      return
    }
    try {
      canvasCtx.drawImage(vid, 0, 0, canvas.width, canvas.height)
    } catch (e) {
      console.warn('drawImage 失败：', e)
    }
    drawRaf = requestAnimationFrame(drawLoop)
  }

  // start recording
  mediaRecorder.start(250)
  isProcessing.value = true
  progress.value = 0

  // play video muted so it can progress
  vid.muted = true
  vid.currentTime = 0
  await vid.play()

  drawRaf = requestAnimationFrame(drawLoop)

  // update progress via timeupdate
  vid.ontimeupdate = () => {
    if (vid.duration) progress.value = Math.min(1, vid.currentTime / vid.duration)
  }

  vid.onended = () => {
    // stop recorder
    try { if (mediaRecorder && mediaRecorder.state !== 'inactive') mediaRecorder.stop() } catch(e) {}
  }
}

function onRecorderStop() {
  // assemble blob
  const blob = new Blob(recordedChunks, { type: 'video/webm' })
  compressedBlob.value = blob
  compressedSize.value = blob.size
  if (compressedUrl.value) URL.revokeObjectURL(compressedUrl.value)
  compressedUrl.value = URL.createObjectURL(blob)

  // cleanup
  cleanupStreams()
  isProcessing.value = false
  progress.value = 1
}

function stopCompress() {
  // stop mediaRecorder
  try { if (mediaRecorder && mediaRecorder.state !== 'inactive') mediaRecorder.stop() } catch(e) {}
  // stop draw loop
  if (drawRaf) { cancelAnimationFrame(drawRaf); drawRaf = null }
  // stop captured stream tracks
  cleanupStreams()
  // stop video playback
  if (originalVideo.value) {
    try { originalVideo.value.pause(); originalVideo.value.currentTime = 0 } catch(e) {}
  }
  isProcessing.value = false
  progress.value = 0
}

function cleanupStreams() {
  if (captureStream) {
    captureStream.getTracks().forEach(t => { try { t.stop() } catch(e){} })
    captureStream = null
  }
  if (canvas) {
    try { canvas.width = 0; canvas.height = 0 } catch(e){}
    canvas = null
    canvasCtx = null
  }
  // 断开并关闭 audioContext
  try {
    if (audioSource) { try { audioSource.disconnect() } catch(e){} audioSource = null }
    if (audioDest) { audioDest = null }
    if (audioCtx) { audioCtx.close().catch(()=>{}); audioCtx = null }
  } catch(e) { /* ignore */ }
  mediaRecorder = null
}

function downloadCompressed() {
  if (!compressedBlob.value) return
  const a = document.createElement('a')
  a.href = compressedUrl.value
  a.download = `compressed_${new Date().toISOString().replace(/[:.]/g,'-')}.webm`
  document.body.appendChild(a)
  a.click()
  a.remove()
}

onUnmounted(() => {
  // revoke urls
  try { if (originalUrl.value) URL.revokeObjectURL(originalUrl.value) } catch(e){}
  try { if (compressedUrl.value) URL.revokeObjectURL(compressedUrl.value) } catch(e){}
  stopCompress()
})
</script>

<style scoped>
/* 保持与项目风格一致的简单样式 */
</style>
